---
title: 中间件
description: 了解如何在 Astro 中使用中间件
i18nReady: true
---
import PackageManagerTabs from '~/components/tabs/PackageManagerTabs.astro';
import { Steps } from '@astrojs/starlight/components';
import Since from '~/components/Since.astro';

**中间件**允许你拦截请求和响应，并在即将渲染页面或端点时动态注入行为。对于所有预渲染的页面，这种渲染发生在构建时，但对于按需渲染的页面，这种渲染发生在请求路由时，这时可以使用 [额外的 SSR 功能如 cookie 和标头](/zh-cn/guides/on-demand-rendering/)。

这也允许你通过修改在所有 Astro 组件和 API 端点中可用的 `locals` 对象，设置和共享跨端点和页面的请求特定信息。即使在构建时运行这个中间件时，这个对象也是可用的。

## 基本用法

<Steps>
1. 创建 `src/middleware.js|ts`（或者，你也可以创建 `src/middleware/index.js|ts`）

2. 在这个文件中，导出一个接收 [`context` 对象](#context-对象) 的 [`onRequest()`](/zh-cn/reference/modules/astro-middleware/#onrequest) 函数。注意这里不能是默认导出。

    ```js title="src/middleware.js"
    export function onRequest (context, next) {
        // 拦截一个请求里的数据
        // 可选地修改 `locals` 中的属性
        context.locals.title = "New title";

        // 返回一个 Response 或者调用 `next()` 的结果
        return next();
    };
    ```

3. 在任何 `.astro` 文件中，使用 `Astro.locals` 访问响应数据。

    ```astro title="src/components/Component.astro"
    ---
    const data = Astro.locals;
    ---
    <h1>{data.title}</h1>
    <p>这个 {data.property} 来自中间件。</p>
    ```
</Steps>

### `context` 对象

[`context`](/zh-cn/reference/api-reference/) 对象包含需要对其他中间件、API 路由和 `.astro` 路由在渲染过程中可用的信息。

它是一个传入 `onRequest()` 的可选参数，可能包含 `locals` 对象以及其他在渲染期间共享的任何属性。例如，`context` 对象可能包含用于认证的 cookies。

### 在 `context.locals` 中存储数据

`context.locals` 是一个可以在中间件中操作、包含来自 `Response` 的数据的对象。

这个 `locals` 对象在请求处理过程中被传递，并作为 [`APIContext`](/zh-cn/reference/api-reference/#locals) 和 [`AstroGlobal`](/zh-cn/reference/api-reference/#locals) 的属性可用。这使得中间件，API 路由和 `.astro` 页面之间可以共享数据。这对于在渲染步骤中存储请求特定的数据（例如用户数据）很有用。

:::tip[集成属性]
[集成](/zh-cn/guides/integrations-guide/)可能会在 `locals` 对象中设置属性和提供一些功能。如果你在使用一个集成，检查它的文档并确认你没有覆盖掉它的属性或者在做重复的工作。
:::

你可以在 `locals` 中存储任何类型的数据：字符串，数字，甚至复杂的数据类型，如函数和映射。

```js title="src/middleware.js"
export function onRequest (context, next) {
    // 拦截一个请求里的数据
    // 可选地修改 `locals` 中的属性
    context.locals.user.name = "John Wick";
    context.locals.welcomeTitle = () => {
        return "Welcome back " + locals.user.name;
    };

    // 返回一个 Response 或者调用 `next()` 的结果
    return next();
};
```

然后你可以在任何 `.astro` 文件中通过 `Astro.locals` 使用这个信息。

```astro title="src/pages/orders.astro"
---
const title = Astro.locals.welcomeTitle();
const orders = Array.from(Astro.locals.orders.entries());
const data = Astro.locals;
---
<h1>{title}</h1>
<p>这个 {data.property} 来自中间件。</p>
<ul>
    {orders.map(order => {
        return <li>{/* do something with each order */}</li>;
    })}
</ul>
```

`locals` 是一个在单个 Astro 路由中创建和销毁的对象；当你的路由页面渲染完后，`locals` 将不再存在，并会创建一个新的。需要在多个页面请求之间保持的信息必须存储在其他地方。

:::note
`locals` 的值不能在运行时被覆盖。这样做会有清除用户存储的所有信息的风险。Astro 会进行检查，如果 `locals` 被覆盖将会抛出错误。
:::

## 示例：删除敏感信息

下面的示例使用中间件将"私人信息"替换为"已删除"，以便你在页面上渲染修改后的 HTML：

```js title="src/middleware.js"
export const onRequest = async (context, next) => {
    const response = await next();
    const html = await response.text();
    const redactedHtml = html.replaceAll("私人信息", "已删除");
    
    return new Response(redactedHtml, {
        status: 200,
        headers: response.headers
    });
};
```

## 中间件类型

你可以导入并使用 `defineMiddleware()` 实用函数来提供类型安全：

```ts
// src/middleware.ts
import { defineMiddleware } from "astro:middleware";

// `context` 和 `next` 会自动被类型化
export const onRequest = defineMiddleware((context, next) => {

});
```

或者，如果你使用 JsDoc 来提供类型安全，你可以使用 `MiddlewareHandler`：

```js
// src/middleware.js
/**
 * @type {import("astro").MiddlewareHandler}
 */
// `context` 和 `next` 会自动被类型化
export const onRequest = (context, next) => {

};
```

要给 `Astro.locals` 内的信息定义类型，也就是在 `.astro` 文件和中间件代码中能提供自动补全，通过在 `env.d.ts` 文件中声明一个全局命名空间来 [扩展全局类型](/zh-cn/guides/typescript/#扩展全局类型)：

```ts title="src/env.d.ts"
type User = {
  id: number;
  name: string;
};

declare namespace App {
  interface Locals {
    user: User;
    welcomeTitle: () => string;
    orders: Map<string, object>;
    session: import("./lib/server/session").Session | null;
  }
}
```

然后，在中间件中，你就可以使用自动补全和类型安全了。

## 中间件链式调用

可以使用 [`sequence()`](/zh-cn/reference/modules/astro-middleware/#sequence) 按指定顺序连接多个中间件：

```js title="src/middleware.js"
import { sequence } from "astro:middleware";

async function validation(_, next) {
    console.log("验证请求");
    const response = await next();
    console.log("验证响应");
    return response;
}

async function auth(_, next) {
    console.log("授权请求");
    const response = await next();
    console.log("授权响应");
    return response;
}

async function greeting(_, next) {
    console.log("问候请求");
    const response = await next();
    console.log("问候响应");
    return response;
}

export const onRequest = sequence(validation, auth, greeting);
```

控制台打印结果顺序如下：

```sh
验证请求
授权请求
问候请求
问候响应
授权响应
验证响应
```

## 重写

<p><Since v="4.13.0" /></p>

`APIContext` 中暴露了一个名为 `rewrite()` 的方法，它的工作方式与 [Astro.rewrite](/zh-cn/guides/routing/#重写) 相同。

使用 `context.rewrite()` 在中间件中显示不同页面的内容，而不会将访问者 [重定向](/zh-cn/guides/routing/#动态重定向) 到新页面。这将触发一个新的渲染阶段，导致任何中间件被重新执行。

```js title="src/middleware.js"
import { isLoggedIn } from "~/auth.js"
export function onRequest (context, next) {
  if (!isLoggedIn(context)) {
    // 如果用户未登录，则更新请求来渲染 `/login` 路由，
    // 并添加标头以指示在成功登录应该把用户重定向到何处。
    // 重新执行中间件。
    return context.rewrite(new Request("/login", {
      headers: {
        "x-redirect-to": context.url.pathname
      }
    }));
  }

  return next();
};
```

你也可以向 `next()` 函数传递一个可选的 URL 路径参数，以重写当前的 `Request` 而不重新触发新的渲染阶段。重写路径的位置可以作为字符串、URL 或 `Request` 提供：

```js title="src/middleware.js"
import { isLoggedIn } from "~/auth.js"
export function onRequest (context, next) {
  if (!isLoggedIn(context)) {
    // 如果用户未登录，则更新请求来渲染 `/login` 路由，
    // 并添加标头以指示在成功登录应该把用户重定向到何处。
    // 将新的 `context` 返回给任何后续中间件。
    return next(new Request("/login", {
      headers: {
        "x-redirect-to": context.url.pathname
      }
    }));
  }

  return next();
};
```

`next` 函数接受与 [`Astro.rewrite()` 函数](/zh-cn/reference/api-reference/#rewrite) 相同的参数。重写路径的位置可以作为字符串、URL 或 `Request` 提供。

当你有多个通过 [sequence()](#中间件链式调用) 链接的中间件函数时，向 `next()` 提交一个路径将在原地重写 `Request`，并且中间件不会再次执行。链中的下一个中间件函数将接收具有其更新的 `context` 的新 `Request`：

使用此签名调用 `next()` 将使用旧的 `ctx.request` 来创建一个新的 `Request` 对象。这意味着无论是在此重写之前还是之后，只要尝试使用 `Request.body`，都会引发运行时错误。[使用 HTML 表单的 Astro Actions](/zh-cn/guides/actions/#从-html-表单操作调用-action) 经常会引发此错误。在这些情况下，我们建议使用 `Astro.rewrite()` 而不是使用中间件来处理 Astro 模板的重写。

```js title="src/middleware.js"
// 当前 URL 是 https://example.com/blog

// 第一个中间件函数
async function first(context, next) {
  console.log(context.url.pathname) // 这里会打印 "/blog"
  // 重写到一个新的路由，首页
  // 返回更新的 `context`，传递给下一个函数
  return next("/")
}

// 当前 URL 仍然是 https://example.com/blog

// 第二个中间件函数
async function second(context, next) {
  // 接收更新后的 `context`
  console.log(context.url.pathname) // 这里会打印  "/"    
  return next()
}

export const onRequest = sequence(first, second);
```

## 错误页面

即使找不到匹配的路由，中间件也会尝试为所有按需渲染的页面运行。这包括 Astro 的默认（空白）404 页面和任何自定义 404 页面。然而，是否运行该代码取决于[适配器](/zh-cn/guides/on-demand-rendering/)。一些适配器可能会提供特定平台的错误页面。

在提供 500 错误页面之前，中间件也会尝试运行，包括自定义的 500 页面，除非服务器错误发生在中间件本身的执行中。如果你的中间件没有成功运行，那么你将无法访问 `Astro.locals` 来渲染你的 500 页面。
